﻿#region Disclaimer/Info
///////////////////////////////////////////////////////////////////////////////////////////////////
// MetaCollection
// 
// MetaCollection is a system to provide a standard structure, permissions and metadata service
// for content organization.
//
// This project is hosted at Google Code at http://code.google.com/p/metacollection/
//
// This project is licensed under the BSD license.  See the License.txt file for more information.
///////////////////////////////////////////////////////////////////////////////////////////////////
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml.Serialization;
using MetaCollection.Framework.Model;

namespace MetaCollection.Framework.Repository
{
	class XmlModelRepository : IModelRepository, IDisposable
	{
		/// <summary>
		/// lock to use when manipulating indexes
		/// </summary>
		static readonly object m_IndexLock = new object();
		/// <summary>
		/// lock to use when read/writing the file.
		/// </summary>
		static readonly object m_FileLock = new object();

		/// <summary>
		/// serializer to write itemspecifications to/from files in xml format
		/// </summary>
		XmlSerializer m_Serializer;
		/// <summary>
		/// path to the file to use for persistence as given by configuration data in Initialize method
		/// </summary>
		string m_FilePath;

		/// <summary>
		/// <c>true</c> if this class has been Initialized.
		/// </summary>
		bool m_Initialized;

		/// <summary>
		/// List of specifications.
		/// </summary>
		List<ItemSpecification> m_Specifications;
		/// <summary>
		/// Position index (hashtable) for guid based access to specifications.
		/// </summary>
		Dictionary<Guid, int> m_IdIndex;
		/// <summary>
		/// Position index (hashtable) for name based access to specifications.
		/// </summary>
		Dictionary<string, int> m_NameIndex;
		/// <summary>
		/// File system watch to monitor our config file for changes and reload as needed.
		/// </summary>
		FileSystemWatcher m_Fsw;

		/// <summary>
		/// Ensures the repository has been initialized.
		/// </summary>
		/// <exception cref="InvalidOperationException">Thrown if <see cref="AdoNetItem.Initialize"/> has not been called.</exception>
		void EnsureInitialized()
		{
			if (!m_Initialized)
				throw new InvalidOperationException("Resource Provider must be initialized prior to use.");
		}

		/// <summary>
		/// Adds the specification to this collection of model specifications.
		/// Maintains the indexes for quick access to the models.
		/// </summary>
		/// <param name="spec">The specification to add.</param>
		void AddSpecification(ItemSpecification spec)
		{
			if ((m_IdIndex.ContainsKey(spec.Id)) || (m_NameIndex.ContainsKey(spec.Name)))
				throw new ArgumentException("Specification already exists.  Each specification must have a unique name and id.", "spec");
			lock (m_IndexLock)
			{
				if ((m_IdIndex.ContainsKey(spec.Id)) || (m_NameIndex.ContainsKey(spec.Name)))
					return;
				m_Specifications.Add(spec);
				m_IdIndex.Add(spec.Id, m_Specifications.Count - 1);
				m_NameIndex.Add(spec.Name, m_Specifications.Count - 1);
			}
		}

		/// <summary>
		/// Adds the collection of specifications to this collection of model specifications.
		/// </summary>
		/// <param name="specs">The specifications to add.</param>
		void AddSpecification(List<ItemSpecification> specs)
		{
			foreach (var spec in specs)
				AddSpecification(spec);
		}

		/// <summary>
		/// Saves the item specifications to a file.
		/// </summary>
		/// <param name="specifications">The specifications.</param>
		void SaveToFile(List<ItemSpecification> specifications, string filePath)
		{
			lock (m_FileLock)
			{
				m_Fsw.EnableRaisingEvents = false;
				using (var file = File.Open(m_FilePath, FileMode.Create))
				{
					m_Serializer.Serialize(file, specifications.Select(s => s.ToStruct()).ToList());
					file.Close();
				}
				m_Fsw.EnableRaisingEvents = true;
			}
		}

		#region IModelRepository Members

		/// <summary>
		/// Initializes a new instance of the Repository
		/// </summary>
		/// <param name="configData"></param>
		public void Initialize(string configData)
		{
			Func<string, string> PathResolve = (System.Web.HttpContext.Current != null ? (Func<string, string>)System.Web.HttpContext.Current.Server.MapPath : Path.GetFullPath);
			if (!m_Initialized)
			{
				m_FilePath = PathResolve(configData);
				m_Specifications = new List<ItemSpecification>();
				m_IdIndex = new Dictionary<Guid, int>();
				m_NameIndex = new Dictionary<string, int>();
				m_Serializer = new XmlSerializer(typeof(List<ItemSpecification>), new XmlRootAttribute("XmlModelRepository"));

				lock (m_FileLock)
				{
					if (File.Exists(m_FilePath))
					{
						using (var file = File.OpenText(m_FilePath))
						{
							AddSpecification((List<ItemSpecification>)m_Serializer.Deserialize(file));
						}
					}
					m_Fsw = new FileSystemWatcher(Path.GetDirectoryName(m_FilePath), Path.GetFileName(m_FilePath));
					m_Fsw.NotifyFilter = NotifyFilters.Size;
					m_Fsw.Changed +=new FileSystemEventHandler(OnConfigurationFileChanged);
				}
				m_Fsw.EnableRaisingEvents = true;
				m_Initialized = true;
			}
		}

		/// <summary>
		/// Called when [configuration file changed].
		/// </summary>
		/// <param name="sender">The sender.</param>
		/// <param name="e">The <see cref="System.IO.FileSystemEventArgs"/> instance containing the event data.</param>
		private void OnConfigurationFileChanged(object sender, FileSystemEventArgs e)
		{
			lock (m_FileLock)
			{
				if (File.Exists(m_FilePath))
				{
					using (var file = File.OpenText(m_FilePath))
					{
						var newList = (List<ItemSpecification>)m_Serializer.Deserialize(file);
						if (newList.Count > 0)
							ResetSpecificationList(newList);
					}
				}
			}
		}

		/// <summary>
		/// Resets the specification list.
		/// </summary>
		/// <param name="newList">The new list.</param>
		private void ResetSpecificationList(List<ItemSpecification> newList)
		{
			lock (m_IndexLock)
			{
				m_Specifications.Clear();
				m_IdIndex.Clear();
				m_NameIndex.Clear();
				foreach (var spec in newList)
				{
					m_Specifications.Add(spec);
					m_IdIndex.Add(spec.Id, m_Specifications.Count - 1);
					m_NameIndex.Add(spec.Name, m_Specifications.Count - 1);
				}
			}
		}

		/// <summary>
		/// Find the provider by Id
		/// </summary>
		/// <param name="id">The id of the provider.</param>
		/// <returns>
		/// An IProviderType record for the given Id or null if no object found
		/// </returns>
		public IItemSpecification FindSpecification(Guid id)
		{
			EnsureInitialized();
			lock (m_IndexLock)
			{
				if (m_IdIndex.ContainsKey(id))
					return m_Specifications[m_IdIndex[id]];
				else
					return null;
			}
		}

		/// <summary>
		/// Finds the resource provider type configuration record given a specific resource type name
		/// </summary>
		/// <param name="name">the name of the resource type handler</param>
		/// <returns>a resource type record</returns>
		public IItemSpecification FindSpecificationByName(string name)
		{
			EnsureInitialized();
			lock (m_IndexLock)
			{
				if (m_NameIndex.ContainsKey(name))
					return m_Specifications[m_NameIndex[name]];
				else
					return null;
			}
		}

		/// <summary>
		/// Puts the model Specification into persistant storage or updates an existing model specification.
		/// </summary>
		/// <param name="item"></param>
		public void SaveSpecification(IItemSpecification item)
		{
			EnsureInitialized();
			// check id index,
			// check name index,
			// check spec list... if found in any of these but not the same in all then we have a problem.
			// if not found then add it.
			AddSpecification(item.ToStruct());
			SaveToFile(m_Specifications, m_FilePath);
		}

		/// <summary>
		/// Finds all registered item specification models in the repository
		/// </summary>
		/// <value></value>
		/// <returns>A collection IItemSpecifications.</returns>
		public IEnumerable<IItemSpecification> RegisteredSpecifications
		{
			get
			{
				lock (m_IndexLock)
				{
					EnsureInitialized();
					return m_Specifications.AsReadOnly().Select((i) => (IItemSpecification)i);
				}
			}
		}

		#endregion

		#region IDisposable Members
		/// <summary>
		/// Disposes managed and unmanaged resources.
		/// </summary>
		public void Dispose()
		{
			if (m_Fsw != null)
				m_Fsw.Dispose();
		}

		#endregion
	}
}
